# -*- coding:utf-8 -*-
"""
彩票数据爬取与模型训练配置文件(含全局日志配置)
集中管理静态配置+日志初始化, 纯文本日志格式
核心功能: 支撑双色球数据爬取、特征工程、多模型训练、预测模拟全流程
"""

import os
import traceback
import requests
from datetime import datetime
from typing import Optional
from urllib3.exceptions import InsecureRequestWarning
from loguru import logger  # 导入loguru的logger(单例模式, 全局唯一日志实例)

# ---------------------- 基础全局配置(全流程通用) ----------------------
# 忽略SSL证书验证警告(爬取网页时避免因证书问题报错)
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# 1. 向上追溯到项目根目录（根据你的工程结构调整os.path.dirname的调用次数）
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 4. 拼接项目根目录+data，得到通用的DATA_FOLDER（无论脚本从哪执行都生效）
DATA_FOLDER = os.path.join(PROJECT_ROOT, "data")

BASE_URL = "https://datachart.500.com/"  # 500彩票网基础域名(数据爬取源头)

TIMEOUT = 10  # 网络请求超时时间(秒), 防止请求阻塞
RETRY_TIMES = 3  # 网络异常时重试次数(提升爬取稳定性)
DELAY = 2  # 爬取间隔(秒), 避免短时间高频请求触发网站反爬机制

LOG_FILE = "ssq_spider.log"  # 日志文件名称
LOG_ROTATION = "10 MB"  # 日志文件切割大小(单个文件达10MB时自动新建文件)
LOG_LEVEL = "INFO"  # 日志级别(DEBUG/INFO/WARNING/ERROR), 生产环境建议INFO及以上

# ---------------------- 双色球(SSQ)专属配置 ----------------------
SSQ_CONFIG = {
    "name": "双色球",  # 彩种名称标识
    "url_pattern": "ssq/history/newinc/history.php?start=1",  # 历史数据分页爬取URL模板
    # 模拟预测参数(用于模型融合后的结果验证)
    "simulation": {
        "n_sim": 10000,  # 蒙特卡洛模拟次数(平衡预测精度与计算效率)
        "confidence_level": 0.95,  # 置信区间置信度(常用95%置信水平)
        "top_k": 6,  # 最终推荐红球数量(匹配双色球6个红球的开奖规则)
        
    },
    "predict_config":{
        "confidence_mode": "top6_mean",        # 置信度计算模, 仅支持'top6_mean'/'max_min_diff'/'top6_sum'
        "confidence_mode": "top6_mean",
        "enable_monte_carlo": True,  # 是否启用蒙特卡罗模拟
        "monte_carlo_n": 100000,      # 模拟次数
        "monte_carlo_top_k": 7       # 输出前7个高频号码
    },
    "csv_header": ["idx", "date", "r1", "r2", "r3", "r4", "r5", "r6", "b1"],  # 原始开奖数据CSV列名(6红1蓝)
    "csv01_header": [  # 01格式化后的数据列名(33个红球+1个蓝球, 1表示出现, 0表示未出现)
        "idx", "date", 'r1','r2','r3','r4','r5','r6','r7','r8','r9','r10',
        'r11','r12','r13','r14','r15','r16','r17','r18','r19','r20',
        'r21','r22','r23','r24','r25','r26','r27','r28','r29','r30',
        'r31','r32','r33', "b1"
    ],
    # 训练目标标签列(33个红球的下一期出现预测, 对应r1_next至r33_next)
    "label_cols": ['r1_next', 'r2_next', 'r3_next', 'r4_next', 'r5_next', 'r6_next', 'r7_next', 'r8_next', 'r9_next', 'r10_next', 'r11_next', 
                   'r12_next', 'r13_next', 'r14_next', 'r15_next', 'r16_next', 'r17_next', 'r18_next', 'r19_next', 'r20_next', 'r21_next', 'r22_next', 
                   'r23_next', 'r24_next', 'r25_next', 'r26_next', 'r27_next', 'r28_next', 'r29_next', 'r30_next', 'r31_next', 'r32_next', 'r33_next'],
    'random_seed': 42,  # 全局随机种子(保证模型训练、数据划分结果可复现)
    'max_red_ball': 33,  # 双色球最大红色号码(1-33)
    'max_blue_ball': 16,  # 双色球最大蓝色号码(1-16)
    'target_cols': 7,  # 目标列总数(6个红球+1个蓝球), 修正原注释错误
    'feature_window_size': [13, 34, 55],
    'missing_strategy ': 'median',  # 缺失值的处理逻辑与具体策略解耦, 填充的策略: mean-均值填充, median-中位数填充, zero-0值填充
    'train_version': f"train_0.2",   # 保存训练数据的目录以及版本信息
    # 分形维数特征配置(用于构建复杂时序特征)
    'fractal': {
        'model_file':   'red_ball_fractal_feature_',  # 分形特征文件前缀(后续拼接窗口期生成文件名)
        'red_fractal_x_box_sizes': [3, 5, 7],  # X轴盒子尺寸(红球位置间隔, 控制空间划分粒度)
        'red_fractal_y_box_sizes': [1, 2, 3],  # Y轴盒子尺寸(号码聚集度间隔, 捕捉分布特征)
        'red_fractal_z_box_sizes': [3, 5, 7],  # Z轴盒子尺寸(适配最小窗口期, 兼顾不同窗口的特征一致性)
    },
    "model_types": ["xgb", "lgb", "rf", "lr", "lstm"],
    # 网页数据解析规则(针对500彩票网HTML结构)
    "parser_rules": {
        "tbody_id": "tdata",  # 存储开奖数据的表格tbody标签ID
        "issue_col_idx": 0,  # 期号在表格中的列索引
        "date_col_idx": 15,  # 开奖日期在表格中的列索引
        "red_ball_col_idxs": list(range(1, 7)),  # 6个红球在表格中的列索引(1-6列)
        "blue_ball_col_idx": 7  # 1个蓝球在表格中的列索引(第7列)
    },
    # 运行模式配置(控制训练流程范围)
    'RUN_MODE': "full",  # "full"=完整流程(训练+调优+融合); "base"=仅基础模型训练; "lightgbm_multi"=仅LightGBM多模型训练
    # 数据处理参数(控制数据划分比例)
    # 核心结论：测试集必须考虑(是判断模型真实预测能力的关键), 但 0.9:0.1:0.1 划分不合适！推荐用 8:1:1 或 7.5:1.5:1(训练集：验证集：测试集), 适配 3500+ 样本量, 既保证训练集足够, 又能让验证集 / 测试集的评估结果稳定可靠
    "train_config": {
        "train_ratio": 0.75,
        "val_ratio": 0.15,  # 验证集比例(总数据的20%用于模型验证)
        "test_ratio": 0.1,  # 测试集比例(总数据的10%用于模型最终测试)
        "early_stopping_rounds": 30,
        "min_delta": 0.0001,
        "save_best": True,
        "log_period": 20,
    },
    # 文件路径配置(统一管理各类文件的存储位置)
    "train_file": {
        "DATA_FOLDER": os.path.join(PROJECT_ROOT, "train_data"),  # 数据存储根目录(存放原始数据、特征文件等)
        "csv_file": "ssq_data.csv",  # 爬取的最近期哦55期原始开奖数据文件
        "csv01_file": "ssq01_formata_data.csv",  # 01格式化后的号码数据文件
        'full_fractal_feature_file': "full_fractal_feature_file.csv",  # 多窗口分形维数特征文件
        'plus30_folder': 'plus30_folder',                  # 单个窗口的特征存放目录
        'full_plus30_features': "full_plus30_features.csv",  # 融合分形特征与基础特征的最终特征文件
        'final_merge_features': "final_merge_features.csv",  # 融合分形特征与基础特征的最终特征文件
        "train_data_path": "train_30plus_features.csv",  # 保存训练集数据
        "val_data_path": "val_30plus_features.csv",  # 保存验证集数据
        "test_data_path": "test_30plus_features.csv",  # 保存测试集数据
        'model_save_dir': os.path.join(PROJECT_ROOT, 'train_models'),  # 模型保存目录(位于data目录的上级目录)
    },
    "predict_file": {
        "DATA_FOLDER": os.path.join(PROJECT_ROOT, "predict_data"),  # 数据存储根目录(存放原始数据、特征文件等)
        "csv_file": "ssq_data.csv",  # 爬取的原始开奖数据文件
        "csv01_file": "ssq01_formata_data.csv",  # 01格式化后的号码数据文件
        'full_fractal_feature_file': "full_fractal_feature_file.csv",  # 多窗口分形维数特征文件
        'plus30_folder': 'plus30_folder',                  # 单个窗口的特征存放目录
        'full_plus30_features': 'full_plus30_features.csv',                  # 单个窗口的特征存放目录
        'final_merge_features': "final_merge_features.csv",  # 融合分形特征与基础特征的最终特征文件
        "train_data_path": "train_30plus_features.csv",  # 保存训练集数据
        "val_data_path": "val_30plus_features.csv",  # 保存验证集数据
        "test_data_path": "test_30plus_features.csv",  # 保存测试集数据
        'model_save_dir': os.path.join(PROJECT_ROOT, 'predict_models'),  # 模型保存目录(位于data目录的上级目录)
        'predict_result': os.path.join(PROJECT_ROOT,'predict_result'),  # 模型保存目录(位于data目录的上级目录)
    },
}

FEATURE_CONDIF = {
    "data_version" : "20251029",
    "feature_data_dir": "feature_data",
    "feature_rule_dir": "feature_rule", 
    "intermediate_dir": "intermediate", 
}

# ---------------------- DDQN强化学习模型配置(可选训练流程) ----------------------
DDQN_CONFIG = {
    "model_save_path": "double_dqn_01_sequence_model.pth",  # DDQN模型保存路径
    "window_size": SSQ_CONFIG['feature_window_size'],  # 时序窗口大小(复用分形窗口期配置)
    "num_predictions": 7,  # 预测列数(固定7列: 6红1蓝)
    "num_episodes": 70000,  # 强化学习训练轮次(保证模型收敛)
    "batch_size": 128,  # 批处理大小
    "lr": 2e-3,  # 初始学习率(控制参数更新速度)
    "gamma": 0.8,  # 折扣因子(平衡即时奖励与未来奖励)
    "eps_start": 0.8,  # 初始探索率(训练初期优先探索)
    "eps_end": 0.3,  # 终止探索率(训练后期降低探索, 提升利用效率)
    "target_update": 100,  # 目标网络更新频率(每100轮更新一次目标网络)
    "save_interval": 5000,  # 模型保存间隔(每5000轮保存一次模型快照)
    "log_interval": 1000,  # 日志输出间隔(每1000轮打印一次训练指标)
    "force_new_training": False,  # 是否强制重新训练(True则覆盖已有模型)
    "patience": 15,  # 早停耐心值(连续15轮无性能提升则停止训练)
    "test_size": 0.15,  # 测试集比例(15%)
    "val_size": 0.15,   # 验证集比例(15%)
    "buffer_periods": 10,  # 数据划分缓冲区(避免未来数据泄露)
}

# ---------------------- 特征工程配置(用于构建模型输入特征) ----------------------
FEATURE_ENGINEER = {
    "freq_window": 50,  # 频率特征统计窗口(统计过去50期号码出现频率)
    "warm_cold_window": 10,  # 温号/冷热号判断窗口(基于过去10期数据)
    "markov_window": 200,  # 马尔可夫链统计窗口(统计过去200期号码转移概率)
    "consecutive_window": 30,  # 连号统计窗口(统计过去30期连号出现规律)
    "cold_threshold": 15,  # 冷号阈值(遗漏期数≥15期判定为冷号)
    "hot_threshold": 0.3,  # 热号阈值(近10期出现频率>30%判定为热号)
    "co_occur_top_k": 5,  # 高频共现对取前5(筛选出现频率最高的5组号码共现组合)
    "repel_top_k": 3  # 排斥组合取前3(筛选出现频率最低的3组号码排斥组合)
}

# ---------------------- 全局日志初始化(核心: 确保仅初始化1次) ----------------------
def init_global_logger(log_file: str = "main.log", pid: Optional[int] = None) -> None:
    """
    初始化全局异步日志(单例模式), 支持多进程安全写入, 避免IO阻塞
    
    Args:
        log_file: 日志文件名
        pid: 进程ID(兼容原参数, 实际优先使用Loguru内置{process.id})
    """
    try:
        # 移除所有现有处理器(避免重复输出)
        logger.remove()
        
        # 1. 控制台输出配置(同步, 保证实时性；多进程下也不会冲突)
        logger.add(
            sink=lambda msg: print(msg, end=""),  # 控制台输出保持同步
            format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level:^5}</level> | <yellow>[PID:{process.id}]</yellow> | <cyan>{message}</cyan>",
            level=LOG_LEVEL,
            colorize=True,
            enqueue=False  # 控制台输出不需要异步, 避免延迟
        )
        
        # 2. 文件输出配置(异步写入, 核心修改：enqueue=True)
        LOG_FOLDER = os.path.join(PROJECT_ROOT, 'logs')
        os.makedirs(LOG_FOLDER, exist_ok=True)
        log_path = os.path.join(LOG_FOLDER, log_file)
        
        logger.add(
            sink=log_path,
            rotation=LOG_ROTATION,  # 日志滚动(避免单个文件过大)
            level=LOG_LEVEL,
            encoding="utf-8",
            format="{time:YYYY-MM-DD HH:mm:ss} | {level:^5} | [PID:{process.id}] | {module}:{function}:{line} | {message}",
            backtrace=True,  # 显示详细回溯信息
            diagnose=True,   # 显示变量值(调试用)
            enqueue=True,    # 关键：开启异步写入, 日志放入队列由后台线程处理
            delay=True       # 延迟创建文件, 直到第一条日志写入(避免空文件)
        )
        
        # 日志初始化完成提示(会异步写入文件, 但控制台同步显示)
        logger.info(f"异步日志初始化完成, 文件路径: {os.path.abspath(log_path)}, 进程ID: {os.getpid()}")
        
    except Exception as e:
        # 极端情况下日志初始化失败, 直接打印到控制台
        print(f"[PID:{os.getpid()}] 异步日志初始化失败: {str(e)}\n{traceback.format_exc()}")

# 初始化主程序日志(默认日志, 其他模块按需重新初始化)
init_global_logger()